package ar.edu.itba.it.cg.grupo01.impl;

import java.awt.Color;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.SwingUtilities;
import javax.vecmath.Point3f;
import javax.vecmath.Vector3f;

import ar.edu.itba.it.cg.grupo01.api.Intersection;
import ar.edu.itba.it.cg.grupo01.api.Ray;
import ar.edu.itba.it.cg.grupo01.api.Intersection.Details;
import ar.edu.itba.it.cg.grupo01.api.image.Image;
import ar.edu.itba.it.cg.grupo01.api.scene.Scene;
import ar.edu.itba.it.cg.grupo01.api.shape.Shape;
import ar.edu.itba.it.cg.grupo01.api.shape.light.Light;
import ar.edu.itba.it.cg.grupo01.impl.gui.ProgressBar;
import ar.edu.itba.it.cg.grupo01.impl.image.ImageImpl;
import ar.edu.itba.it.cg.grupo01.impl.scene.optimization.Octree;
import ar.edu.itba.it.cg.grupo01.impl.shape.Plane;
import ar.edu.itba.it.cg.grupo01.impl.shape.shader.Shader;


public class RayTracer {

	private int depth;
	private static final float DELTA = 0.001f;

	private Scene scene;
	private Camera camera;

	public RayTracer(Scene scene, Camera camera, int depth) {
		this.scene = scene;
		this.camera = camera;
		this.depth = depth;
	}

	int progress = 0;
	int cantPixels = 0;
	int pixelsDone = 0;

	public Image drawImage(RayTracerOptions options, int typeIntRgb) {

		ProgressBar pbr = null;
		
		if( options.isShowProgress()) {
			pbr = new ProgressBar();
		}

		Image image = new ImageImpl(camera.getxResolution(), camera
				.getyResolution(), typeIntRgb);
		camera.setIterator(new SingleRay(camera));
		cantPixels = camera.getxResolution() * camera.getyResolution();

		// Build octree
		Octree octree = new Octree(scene.getShapes(), new IntersectionFactory());
		for (CameraRay ray : camera) {
			Color color = getColor(octree, scene.getPlanes(), ray);
			image.setPixel(ray.getX(), ray.getY(), color);

			if( options.isShowProgress() ) {
				updateProgressBar(pbr);
			}

		}

		if( options.isShowProgress() ) {
			pbr.closeWindow();
		}

		return image;
	}

	public Image drawImageAntiAliased(RayTracerOptions options, int typeIntRgb,
			int n) {

		ProgressBar pbr = null;
		
		if( options.isShowProgress()) {
			pbr = new ProgressBar();
		}
		
		Image image = new ImageImpl(camera.getxResolution(), camera
				.getyResolution(), typeIntRgb);

		cantPixels = camera.getxResolution() * camera.getyResolution();
		n = (n < 0) ? 0 : n;
		camera.setIterator(new MultipleRays(camera, n));
		int qty = (int) Math.pow(2, 2 * n);

		MultipleRays iterator = (MultipleRays) camera.iterator();
		List<Color> colors = new ArrayList<Color>();

		// Build octree
		Octree octree = new Octree(scene.getShapes(), new IntersectionFactory());
		for (CameraRay ray : camera) {
			for (int i = 1; i < qty; i++) {
				Color color = getColor(octree, scene.getPlanes(), ray);
				colors.add(color);
				ray = iterator.next();
			}
			Color color = getColor(octree, scene.getPlanes(), ray);
			colors.add(color);

			Color averageColor = Utilities.averageColors(colors);
			image.setPixel(ray.getX(), ray.getY(), averageColor);
			
			if( options.isShowProgress() ) {
				updateProgressBar(pbr);
			}		
			
			colors.clear();
		}

		if( options.isShowProgress() ) {
			pbr.closeWindow();
		}		
		return image;
	}

	private void updateProgressBar(final ProgressBar pbr) {
		pixelsDone++;

		progress = (int) ((pixelsDone / (float) cantPixels) * 100.0);

		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				pbr.updateProgressBarValue(progress);
			}
		});
	}

	private Color getColor(Octree octree, List<Plane> planes, Ray ray) {

		ColorHolder colorHolder = new ColorHolder();

		rayTrace(octree, planes, ray, 0, colorHolder);

		return colorHolder.getColor();
	}

	private boolean rayTrace(Octree octree, List<Plane> planes, Ray ray,
			int currentDepth, ColorHolder colorHolder) {

		// Base case of recursion, we are done
		if (currentDepth == this.depth) {
			return false;
		}

		Intersection<? extends Shape> allIntersections = findAllIntersections(
				ray, octree, planes);

		return rayTrace(octree, planes, ray, currentDepth, allIntersections,
				colorHolder);

	}

	private Intersection<? extends Shape> findAllIntersections(Ray ray,
			Octree octree, List<Plane> planes) {

		Set<Intersection<? extends Shape>> planeIntersections = new HashSet<Intersection<? extends Shape>>();

		for (Plane plane : planes) {
			planeIntersections.add(plane.intersects(ray));
		}

		Intersection<? extends Shape> octreeIntersection = octree
				.intersects(ray);
		planeIntersections.add(octreeIntersection);

		Intersection<? extends Shape> allIntersections = Intersection.FACTORY
				.createIntersection(planeIntersections);
		return allIntersections;
	}

	private Intersection<? extends Shape> hitsLight(Ray lightRay,
			Octree octree, List<Plane> planes, Shape shape) {
		Set<Intersection<? extends Shape>> planeIntersections = new HashSet<Intersection<? extends Shape>>();

		for (Plane plane : planes) {
			if( ! plane.equals(shape) ) {
				planeIntersections.add(plane.intersects(lightRay));
			}
		}

		Intersection<? extends Shape> octreeIntersection = octree
				.hitsLight(lightRay,shape);
		planeIntersections.add(octreeIntersection);

		Intersection<? extends Shape> allIntersections = Intersection.FACTORY
				.createIntersection(planeIntersections);
		return allIntersections;
	}
	
	private boolean rayTrace(Octree octree, List<Plane> planes, Ray ray,
			int currentDepth, Intersection<? extends Shape> allIntersections,
			ColorHolder colorHolder) {
		// If the ray does not intersects anything, then we are done
		if (!allIntersections.intersects()) {
			return false;
		}
		Intersection<? extends Shape> intersection = allIntersections
				.getClosestIntersectionTo(ray.getOrigin());

		return rayTrace(octree, planes, ray, intersection, currentDepth,
				colorHolder);
	}

	private boolean rayTrace(Octree octree, List<Plane> planes, Ray ray,
			Intersection<? extends Shape> intersection, int currentDepth,
			ColorHolder colorHolder) {

		Shape shape = intersection.getObject();
		RayTraceInformation info = new RayTraceInformation(this, scene, octree,
				ray, colorHolder, shape, intersection);

		Shader shader = shape.getShader();
		Details details = new Details();
		details.reflectionK = shader.getReflectionK();
		details.refractionK = shader.getRefractionK();
		details.skipRefract = false;
		
		doRefraction(info, intersection, currentDepth, details);

		doReflection(info, details, currentDepth);

		shader.shade(info);

		if (details.reflectionK > 0.0) {
			colorHolder.add(info.getReflectionColor());
		}

		if (details.refractionK > 0.0) {
			colorHolder.add(info.getRefractionColor());
		}

		/* TODO BORRAR
		if( colorHolder.getColor().equals(new Color(63,63,63)) ) {
			System.out.println(shape.getClass());
			System.out.println(shape.getShader());
			System.out.println(info.getReflectionColor());
			System.out.println(info.getRefractionColor());
			System.out.println("-----------------------------");
		}
		*/
		return true;
	}

	private void doRefraction(RayTraceInformation info,
			Intersection<? extends Shape> intersection, int currentDepth, Details details) {

		Shape shape = info.getShape();
		
		if (details.refractionK > 0.0) {
			intersection.getDetails(shape, info, DELTA, details);

			if (!details.skipRefract) {
				ColorHolder refractionColor = new ColorHolder();

				if (rayTrace(info.getOctree(), info.getScene().getPlanes(),
						details.refractedRay, currentDepth + 1, refractionColor)) {
					refractionColor.mul(details.refractionK);
					info.setRefractionColor(refractionColor);
				}
			}
		}
	}

	private void doReflection(RayTraceInformation info,
			Intersection.Details details, int currentDepth) {

		Intersection<? extends Shape> intersection = info.getIntersection();
		if (details.reflectionK > 0
				&& ((!(details.refractionK > 0)) || details.skipRefract || !intersection
						.getType().equals(Intersection.Type.IN))) {
			ColorHolder reflectionColor = new ColorHolder();
			Vector3f reflectionDir = info.getReflectedDirection(intersection
					.getType());

			Point3f origin = info.getIntersectionPoint();

			origin.x += DELTA * reflectionDir.x;
			origin.y += DELTA * reflectionDir.y;
			origin.z += DELTA * reflectionDir.z;

			Ray reflectedRay = new RayImpl(origin, reflectionDir);

			if (rayTrace(info.getOctree(), info.getScene().getPlanes(),
					reflectedRay, currentDepth + 1, reflectionColor)) {
				reflectionColor.mul(details.reflectionK);
			}
			info.setReflectionColor(reflectionColor);
		}

	}

	public static final class ColorHolder {
		private float r = 0.0f;
		private float g = 0.0f;
		private float b = 0.0f;

		public ColorHolder(Color color) {
			set(color);
		}

		public ColorHolder() {
		}

		public Color getColor() {
			check();
			return new Color(r, g, b);
		}

		private void check() {
			r = r > 1.0f ? 1.0f : r;
			g = g > 1.0f ? 1.0f : g;
			b = b > 1.0f ? 1.0f : b;
		}

		public void add(ColorHolder color) {

			r += color.r;
			g += color.g;
			b += color.b;

			check();
		}

		public void mul(float factor) {

			float floatFactor = (float) factor;

			r *= floatFactor;
			g *= floatFactor;
			b *= floatFactor;

			check();
		}

		public void add(Color color) {
			r += color.getRed() / 255f;
			g += color.getGreen() / 255f;
			b += color.getBlue() / 255f;

			check();
		}

		public void mul(Color color) {

			r *= color.getRed() / 255f;
			g *= color.getGreen() / 255f;
			b *= color.getBlue() / 255f;

			check();
		}

		public void madd(float factor, Color color) {
			float floatFactor = (float) factor;

			r += floatFactor * (color.getRed() / 255f);
			g += floatFactor * (color.getGreen() / 255f);
			b += floatFactor * (color.getBlue() / 255f);

			check();

		}

		public void set(Color color) {
			r = color.getRed() / 255f;
			g = color.getGreen() / 255f;
			b = color.getBlue() / 255f;
		}

		@Override
		public String toString() {
			return "ColorHolder [b=" + b + ", g=" + g + ", r=" + r + "]";
		}

	}

	public boolean hitsLight(Shape shape, Light light, Ray lightRay,
			Octree octree, List<Plane> planes) {

		Intersection<? extends Shape> allIntersections = hitsLight(
				lightRay, octree, planes, shape);

		if (!allIntersections.intersects()) {
			return true;
		}

		Point3f rayOrigin = lightRay.getOrigin();

		Intersection<? extends Shape> closestIntersection = allIntersections
				.getClosestIntersectionTo(rayOrigin);

		Point3f closest = closestIntersection.closestTo(rayOrigin);

		float closestDistance = closest.distance(rayOrigin);
		float lightDistance = light.getSource().distance(rayOrigin);

		return (Double.compare(lightDistance, closestDistance) < 0) ? true
				: false;
	}
	

	public static class RayTracerOptions {

		private final String sceneName;
		private int width = 640;
		private int height = 480;
		private float fov = 60.0f;
		// TODO this is the default????
		private int dof = 1;

		private String outputFile = null;
		private boolean showTime = false;
		private boolean showProgress = false;
		private boolean showGUI = false;
		private boolean showImage = false;

		public RayTracerOptions(final String sceneName) {
			this.sceneName = sceneName;

			// Output defaults to sceneName.png
			String[] splitName = sceneName.split("\\.");
			this.outputFile = splitName[0] + ".png";
		}

		public String getSceneName() {
			return sceneName;
		}

		public int getWidth() {
			return width;
		}

		public int getHeight() {
			return height;
		}

		public float getFov() {
			return fov;
		}

		public String getOutputFile() {
			return outputFile;
		}

		public boolean isShowTime() {
			return showTime;
		}

		public int getDof() {
			return dof;
		}

		public boolean isShowProgress() {
			return showProgress;
		}

		public boolean isShowGUI() {
			return showGUI;
		}

		public boolean isShowImage() {
			return showImage;
		}

		public void setWidth(int width) {
			this.width = width;
		}

		public void setHeight(int height) {
			this.height = height;
		}

		public void setFov(float fov) {
			this.fov = fov;
		}

		public void setOutputFile(String outputFile) {
			this.outputFile = outputFile;
		}

		public void setShowTime(boolean showTime) {
			this.showTime = showTime;
		}

		public void setDof(int dof) {
			this.dof = dof;
		}

		public void setShowProgress(boolean showProgress) {
			this.showProgress = showProgress;
		}

		public void setShowGUI(boolean showGUI) {
			this.showGUI = showGUI;
		}

		public void setShowImage(boolean showImage) {
			this.showImage  = showImage;
		}

	}

}
